/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package domain.player.ai;

import domain.GameGrid;
import domain.player.*;

public class AI_Minimax extends AI {
    /**
     * The depth of the search tree when checking for good moves
     */
    public static final int DEFAULT_DEPTH = 5;
    /**
     * The winning constant. It's the same for both the max and min player and
     * used mostly in the state evaluation class
     */
    private static final int INFINITY = 200000000;
    /**
     * Default choice. If it's still at DEFAULT_CHOICE after checking all moves,
     * a random choice is made.
     */
    protected static final int WINNING = 190000000;
    /**
     * The maximum amount of moves ahead it searches
     */
    private static final int DEFAULT_CHOICE = -1;
    private int ply = DEFAULT_DEPTH;
	private boolean debug = false;
	private boolean alphaBeta = true;

    private Evaluation ai;
    private static int[] columnPriority;
    private int nrOfTriedMoves = 0;
    private int moveScore = 0;

    public AI_Minimax(Disc discID, boolean debug, boolean alphaBeta, int ply) {
        super(discID);

		this.debug = debug;
		this.alphaBeta = alphaBeta;
		this.ply = ply;

        ai = new Evaluation();

        columnPriority = new int[GameGrid.GRID_COLUMNS];
        initPriority();
    }
    
    public AI_Minimax(Disc discID) {
        super(discID);

        ai = new Evaluation();

        columnPriority = new int[GameGrid.GRID_COLUMNS];
        initPriority();
    }
    
    @Override
    public int getType() {
        return AI.AI_MINIMAX;
    }

    public void setPly(int depth) {
        this.ply = depth;
    }

	public void setDebug(boolean debug) {
        this.debug = debug;
    }

    public int getPly() {
        return ply;
    }

    /**
     * Sets ai so it uses the grid that is provided and returns the best column
     * in the current game state.
     *
     * @param grid the current game state/GameGrid
     * @return the best column to choose it finds
     */
    @Override
    public int getGoodMove(GameGrid grid) {
        ai.setGameGrid(grid);
        // System.out.println("minimax uses\n" + grid.toString()); // comment this line to stop print grid
        nrOfTriedMoves = 0;
        return getGoodColumn();
    }

    /**
     * Tests all different moves you can make and checks which one gives you the
     * most points, assuming your opponent will make the best move possible (for
     * him) in every state.
     *
     * @return An integer representing the best column to pick.
     */
    private int getGoodColumn() {
        // System.out.println("" + super.getDiscID());
        int bestChoice = DEFAULT_CHOICE;
        int bestPoints = -INFINITY,
                points = 0;

        // tests all valid moves, saves the best one.
        for (int i = 0; i < GameGrid.GRID_COLUMNS; i++) {
            // if the move is valid, test it.
            if (ai.isValidMove(columnPriority[i])) {
                nrOfTriedMoves++;
                // try setting the disc.
                ai.setDisc(columnPriority[i], getDiscID());

                points = ai.evalState(getDiscID());
                if (points != WINNING) {
                    points = calcPoints(points, minValue(1, otherDisc(getDiscID())));
                }

                if (points > bestPoints) {
                    bestChoice = columnPriority[i];
                    bestPoints = points;
                }

                ai.removeDisc(columnPriority[i]);
            }
            moveScore = bestPoints;
        }

        return bestChoice;
    }
    
    
    private int getGoodColumnAB() {
        // System.out.println("" + super.getDiscID());
        int bestChoice = DEFAULT_CHOICE;
        int bestPoints = -INFINITY,
                points = 0;
        int alpha = -INFINITY;
        int beta = INFINITY;

        // tests all valid moves, saves the best one.
        for (int i = 0; i < GameGrid.GRID_COLUMNS; i++) {
            // if the move is valid, test it.
            if (ai.isValidMove(columnPriority[i])) {
                nrOfTriedMoves++;
                // try setting the disc.
                ai.setDisc(columnPriority[i], getDiscID());

                points = ai.evalState(getDiscID());
                if (points != WINNING) {
                    points = calcPoints(points, minAlphaBeta(1, otherDisc(getDiscID()), alpha, beta));
                }

                if (points > bestPoints) {
                    bestChoice = columnPriority[i];
                    bestPoints = points;
                }

                ai.removeDisc(columnPriority[i]);
            }
            moveScore = bestPoints;
        }

        return bestChoice;
    }

    
    /*
     * 
     * the old one
     * private int getGoodColumn() {
        int bestChoice = DEFAULT_CHOICE;
        int bestPoints = -INFINITY,
                points = 0;

        // tests all valid moves, saves the best one.
        for (int i = 0; i < GameGrid.GRID_COLUMNS; i++) {
            // if the move is valid, test it.
            if (ai.isValidMove(columnPriority[i])) {
                nrOfTriedMoves++;
                // try setting the disc.
                ai.setDisc(columnPriority[i], getDiscID());

                points = ai.evalState(getDiscID());
                if (points != WINNING) {
                    points = calcPoints(points, maxValue(1, otherDisc(getDiscID())));
                }

                if (points > bestPoints) {
                    bestChoice = columnPriority[i];
                    bestPoints = points;
                }

                ai.removeDisc(columnPriority[i]);
            }
            moveScore = bestPoints;
        }

        if (bestChoice == DEFAULT_CHOICE) {
            return pickRandomColumn(ai.getGameGrid());
        } else {
            return bestChoice;
        }
    }
     * 
     * 
     * 
     * 
     */
    
    /**
     * the max value function
     *
     * @return: a utility value
     */
    private int maxValue(int depth, Disc disc){
        int points = -INFINITY;
        int maxPoints = points;

        if (depth > ply || ai.isTie()) {
            points = ai.evalState(disc);
            // System.out.println("points = " + points);
            return points;
        }
        
        for(int i = 0; i < GameGrid.GRID_COLUMNS; i++){
            if(ai.isValidMove(columnPriority[i])){
                nrOfTriedMoves++;
                //System.out.println(nrOfTriedMoves);
                ai.setDisc(columnPriority[i], disc);
                points = ai.evalState(disc);
                // if you can win, why continue the search?
                if (points == WINNING) {
                    ai.removeDisc(columnPriority[i]);
                    // System.out.println("max can win");
                    return (WINNING - depth);
                }
                /**/
                points = minValue(depth + 1, otherDisc(disc));

                /*
                 * removes the added disc from the grid so it won't interfere
                 * when you test other moves.
                 */
                ai.removeDisc(columnPriority[i]);

                if (points > maxPoints) {
                    maxPoints = points;
                }

            }
        }
        // System.out.println("returnerar max = " + maxPoints);
        return maxPoints;
    }
    
    private int maxAlphaBeta(int depth, Disc disc, int alpha, int beta) {
        int points = -INFINITY;
        int maxPoints = points;

        if (depth > ply || ai.isTie()) {
            points = ai.evalState(disc);
            // System.out.println("points = " + points);
            return points;
        }
        
        for(int i = 0; i < GameGrid.GRID_COLUMNS; i++){
            if(ai.isValidMove(columnPriority[i])){
                nrOfTriedMoves++;
                //System.out.println(nrOfTriedMoves);
                ai.setDisc(columnPriority[i], disc);
                points = ai.evalState(disc);
                // if you can win, why continue the search?
                if (points == WINNING) {
                    ai.removeDisc(columnPriority[i]);
                    // System.out.println("max can win");
                    return (WINNING - depth);
                }
                /**/
                points = minAlphaBeta(depth + 1, otherDisc(disc), alpha, beta);

                /*
                 * removes the added disc from the grid so it won't interfere
                 * when you test other moves.
                 */
                ai.removeDisc(columnPriority[i]);

                if (points > maxPoints) {
                    maxPoints = points;
                }
                
                if(points >= alpha){
                    alpha = points;
                }
                
                if(beta <= alpha){
                    break;
                }
            }
        }
        // System.out.println("returnerar max = " + maxPoints);
        return maxPoints;
    }
    
    private int minAlphaBeta(int depth, Disc disc, int alpha, int beta) {
        int points = INFINITY;
        int minPoints = points;

        if (depth > ply || ai.isTie()) {
            points = ai.evalState(disc); // evaluerar alltid poängen från en viss persons synpunkt oavsett vem som "kör" minimaxen
            // System.out.println("points = " + points + " i min");
            return points;
        }

        for(int i = 0; i < GameGrid.GRID_COLUMNS; i++){
            if(ai.isValidMove(columnPriority[i])){
                nrOfTriedMoves++;
                ai.setDisc(columnPriority[i], disc);
                points = ai.evalState(disc);
                // if you can win, why continue the search?
                if (points == WINNING) {
                    ai.removeDisc(columnPriority[i]);
                    // System.out.println("min can win");
                    return -(WINNING - depth);
                }
                /**/
                points = maxAlphaBeta(depth + 1, otherDisc(disc), alpha, beta);

                // points = calcPoints(points, maxValue(depth + 1, otherDisc(disc))); // eventuell bugg här

                /*
                 * removes the added disc from the grid so it won't interfere
                 * when you test other moves.
                 */
                ai.removeDisc(columnPriority[i]);

                if (points < minPoints) {
                    minPoints = points;
                }
                
                if(points < beta){
                    beta = points;
                }
                
                if(beta <= alpha){
                    break;
                }

            }
        }
        
        // System.out.println("returnerar min = " + minPoints);
        return minPoints;
    }


    /**
     * the min value function
     *
     * @return: a utility value
     */
    private int minValue(int depth, Disc disc){
        int points = INFINITY;
        int minPoints = points;

        if (depth > ply || ai.isTie()) {
            points = ai.evalState(disc); // evaluerar alltid poängen från en viss persons synpunkt oavsett vem som "kör" minimaxen
            // System.out.println("points = " + points + " i min");
            return points;
        }

        for(int i = 0; i < GameGrid.GRID_COLUMNS; i++){
            if(ai.isValidMove(columnPriority[i])){
                nrOfTriedMoves++;
                ai.setDisc(columnPriority[i], disc);
                points = ai.evalState(disc);
                // if you can win, why continue the search?
                if (points == WINNING) {
                    ai.removeDisc(columnPriority[i]);
                    // System.out.println("min can win");
                    return -(WINNING - depth);
                }
                /**/
                points = maxValue(depth + 1, otherDisc(disc));

                // points = calcPoints(points, maxValue(depth + 1, otherDisc(disc))); // eventuell bugg här

                /*
                 * removes the added disc from the grid so it won't interfere
                 * when you test other moves.
                 */
                ai.removeDisc(columnPriority[i]);

                if (points < minPoints) {
                    minPoints = points;
                }

            }
        }
        
        // System.out.println("returnerar min = " + minPoints);
        return minPoints;
    }







    /**
     * Minimax with alpha-beta cutoffs. Alpha is the minimum score the current
     * player can get. Beta is maximum score the opponent can get.
     */
    private int minMaxAlphaBeta(int depth, Disc disc) {
        // variables are initialized to the worst possible values they can ever have.
        int points = -INFINITY;
        int maxPoints = -INFINITY;

        if (depth > ply || ai.isTie()) {
            return 0;
        }

        for (int i = 0; i < GameGrid.GRID_COLUMNS; i++) {
            if (ai.isValidMove(columnPriority[i])) {
                nrOfTriedMoves++;
                /*
                 * sets a disc to the grid
                 */
                ai.setDisc(columnPriority[i], disc);

                points = ai.evalState(disc);

                // if you can win, why continue the search?
                if (points == WINNING) {
                    ai.removeDisc(columnPriority[i]);
                    return (WINNING - depth);
                }

                points = calcPoints(points, minMaxAlphaBeta(depth + 1, otherDisc(disc)));

                /*
                 * removes the added disc from the grid so it won't interfere
                 * when you test other moves.
                 */
                ai.removeDisc(columnPriority[i]);

                if (points > maxPoints) {
                    maxPoints = points;
                }
            }
        }

        return maxPoints;
    }

    public int getNrOfTriedMoves() {
        return nrOfTriedMoves;
    }

    private Disc otherDisc(Disc discID) {
        return (discID == Disc.ONE) ? Disc.TWO : Disc.ONE;
    }
    
    /*
     * Man får samma poäng både om man vinner nästa runda eller förlorar nästa runda vilket känns jävligt weird
     */
    private int calcPoints(int ownPoints, int opponentPoints) {
        // opponent is winning on the next turn,
        if (opponentPoints >= (WINNING - ply)) {
            // System.out.println("opponent winning next");
            return -opponentPoints;
        }
        // you are winning in the next turns.
        if (opponentPoints <= ((-WINNING) + ply)) {
            // System.out.println("winning next turn");
            return opponentPoints;
        }

        return (ownPoints - opponentPoints);
    }

    /**
     * Initiates the columnPriority vector so that the middle column index comes
     * first, followed by the ones closests to the center and so on.
     *
     * That way the evaluation algorithm doesn't start by looking at the column
     * at a corner, since it should be more beneficial to place a disc in the
     * middle columns if you want to make good moves (in case all moves you can
     * make returns the same result).
     */
    private void initPriority() {
        int i = 0, j = 0,
                middleIndex = ((int) ((GameGrid.GRID_COLUMNS - 1) / 2));

        // if there's an uneven number of columns.
        if (GameGrid.GRID_COLUMNS % 2 == 1) {
            columnPriority[i] = middleIndex;
            i++;
            j++;
        }

        for (; i < GameGrid.GRID_COLUMNS; i++) {
            if (i % 2 == 0) {
                columnPriority[i] = middleIndex - j;
                j++;
            } else {
                columnPriority[i] = middleIndex + j;
            }
        }
    }

    public int getMoveScore(){
        return moveScore;
    }
}